using System;
using System.Linq.Expressions;
using System.Reflection.Emit;

namespace SmartClientPlatform.RemoteClientBase
{
    internal static class PropertyDelegate
    {
        public static void SetValue<TDataObject, TProperty>(
            this TDataObject dataObject, Expression<Func<TDataObject, TProperty>> expression, 
            TProperty propertyValue)
        {
            //TODO: implement this
            throw new NotImplementedException();
        }

        public static TProperty GetValue<TDataObject, TProperty>(
            this TDataObject dataObject, 
            Expression<Func<TDataObject, TProperty>> expression)
        {
            //TODO: implement this
            return expression.Compile()(dataObject);
        }

        private static readonly Cache<string, CacheValue> cache = new Cache<string, CacheValue>();

        public static void SetValue<TDataObject>(
            this TDataObject dataObject, string propertyName, object propertyValue)
        {
            var dataObjecType = typeof(TDataObject);

            var key = GetKey(dataObjecType, propertyName);

            cache.Find(key, new SetterFindHandler<TDataObject>(
                                dataObject, propertyValue,
                                dataObjecType, propertyName),
                       cacheValue => cacheValue.Setter);
        }

        private static string GetKey(Type dataObjecType, string propertyName)
        {
            return string.Format("{0}|{1}", dataObjecType, propertyName);
        }

        private class CacheValue
        {
            public ISetter Setter { get; set; }

            public IGetter Getter { get; set; }
        }

        private class SetterFindHandler<TDataObject> : IFindHandler<CacheValue, ISetter>
        {
            private readonly TDataObject dataObject;
            private readonly object propertyValue;
            private readonly Type dataObjecType;
            private readonly string propertyName;

            public SetterFindHandler(TDataObject dataObject, object propertyValue,
                Type dataObjecType, string propertyName)
            {
                this.dataObject = dataObject;
                this.propertyValue = propertyValue;
                this.dataObjecType = dataObjecType;
                this.propertyName = propertyName;
            }

            public void OnSlotFind(ISetter slot)
            {
                slot.SetValue(dataObject, propertyValue);
            }

            public Action OnFindEmptySlot(ISlotWriter<CacheValue> slotWriter)
            {
                return CreateSettor(setter => slotWriter.Write(cacheValue => cacheValue.Setter = setter));
            }

            public Action OnNotFind(IAdder<CacheValue> adder)
            {
                return CreateSettor(setter => adder.Add(new CacheValue { Setter = setter }));
            }

            private Action CreateSettor(Action<ISetter> addToCacheAction)
            {
                var propertyValueType = dataObjecType.GetProperty(propertyName).PropertyType;

                var dynamicMethod = new DynamicMethod(
                    "DynamicSetValue",
                    typeof(void),
                    new[] { dataObjecType, propertyValueType },
                    typeof(PropertyDelegate).Module);

                var ilGenerator = dynamicMethod.GetILGenerator();
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Ldarg_1);
                ilGenerator.Emit(OpCodes.Call,
                                 dataObjecType.GetMethod(
                                     string.Format("set_{0}", propertyName),
                                     new[] { propertyValueType }));
                ilGenerator.Emit(OpCodes.Ret);


                var typedSetter = (ITypedSetter<TDataObject>)
                               Activator.CreateInstance(
                                   typeof(DelegateSetterWrapper<,>).MakeGenericType(
                                       dataObjecType, propertyValueType),
                                   dynamicMethod.CreateDelegate(
                                       typeof(Action<,>).MakeGenericType(
                                           dataObjecType, propertyValueType)));

                addToCacheAction(typedSetter);

                return () => typedSetter.TypedSetValue(dataObject, propertyValue);
            }
        }

        //public for Activator
        public class DelegateSetterWrapper<TDataObject, TPropertyValue> : ITypedSetter<TDataObject>
        {
            private readonly Action<TDataObject, TPropertyValue> action;

            public DelegateSetterWrapper(Delegate _delegate)
            {
                action = (Action<TDataObject, TPropertyValue>)_delegate;
            }

            public void TypedSetValue(TDataObject dataObject, object value)
            {
                action(dataObject, (TPropertyValue)value);
            }

            public void SetValue(object dataObject, object value)
            {
                TypedSetValue((TDataObject)dataObject, value);
            }
        }

        private interface ITypedSetter<TDataObject> : ISetter
        {
            void TypedSetValue(TDataObject dataObject, object value);
        }

        private interface ISetter
        {
            void SetValue(object dataObject, object value);
        }

        public static object GetValue<TDataObject>(this TDataObject dataObject, string propertyName)
        {
            var dataObjecType = typeof(TDataObject);

            var key = GetKey(dataObjecType, propertyName);

            object result = null;
            cache.Find(key, new GetterFindHandler<TDataObject>(
                                arg => result = arg, dataObject,
                                dataObjecType, propertyName),
                       cacheValue => cacheValue.Getter);
            return result;
        }

        private class GetterFindHandler<TDataObject> : IFindHandler<CacheValue, IGetter>
        {
            private readonly Action<object> resultAction;
            private readonly TDataObject dataObject;
            private readonly Type dataObjecType;
            private readonly string propertyName;

            public GetterFindHandler(Action<object> resultAction, TDataObject dataObject,
                Type dataObjecType, string propertyName)
            {
                this.resultAction = resultAction;
                this.dataObject = dataObject;
                this.dataObjecType = dataObjecType;
                this.propertyName = propertyName;
            }

            public void OnSlotFind(IGetter slot)
            {
                resultAction(slot.GetValue(dataObject));
            }

            public Action OnFindEmptySlot(ISlotWriter<CacheValue> slotWriter)
            {
                return CreateGettor(getter => slotWriter.Write(cacheValue => cacheValue.Getter = getter));
            }

            public Action OnNotFind(IAdder<CacheValue> adder)
            {
                return CreateGettor(getter => adder.Add(new CacheValue { Getter = getter }));
            }

            private Action CreateGettor(Action<IGetter> addToCacheAction)
            {
                var propertyValueType = dataObjecType.GetProperty(propertyName).PropertyType;

                var dynamicMethod = new DynamicMethod(
                    "DynamicGetValue",
                    propertyValueType,
                    new[] { dataObjecType },
                    typeof(PropertyDelegate).Module);

                var generator = dynamicMethod.GetILGenerator();
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Call, dataObjecType.GetMethod(string.Format("get_{0}", propertyName),
                                                               new Type[] { }));
                generator.Emit(OpCodes.Ret);

                var typedGetter = (ITypedGetter<TDataObject>)
               Activator.CreateInstance(
                   typeof(DelegateGetterWrapper<,>).MakeGenericType(
                       dataObjecType, propertyValueType),
                   dynamicMethod.CreateDelegate(
                       typeof(Func<,>).MakeGenericType(
                           dataObjecType, propertyValueType)));

                addToCacheAction(typedGetter);

                return () => resultAction(typedGetter.TypedGetValue(dataObject));
            }
        }

        //public for Activator
        public class DelegateGetterWrapper<TDataObject, TPropertyValue> : ITypedGetter<TDataObject>
        {
            private readonly Func<TDataObject, TPropertyValue> func;

            public DelegateGetterWrapper(Delegate _delegate)
            {
                func = (Func<TDataObject, TPropertyValue>)_delegate;
            }

            public object GetValue(object dataObject)
            {
                return TypedGetValue((TDataObject)dataObject);
            }

            public object TypedGetValue(TDataObject dataObject)
            {
                return func(dataObject);
            }
        }

        private interface ITypedGetter<TDataObject> : IGetter
        {
            object TypedGetValue(TDataObject dataObject);
        }

        private interface IGetter
        {
            object GetValue(object dataObject);
        }
    }
}